/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.hop.workflow.actions.mailvalidator;

import org.apache.commons.validator.GenericValidator;
import org.apache.hop.core.logging.ILogChannel;
import org.apache.hop.core.util.Utils;
import org.apache.hop.i18n.BaseMessages;

import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.io.*;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Hashtable;

public class MailValidation {

  private static final Class<?> PKG = ActionMailValidator.class; // For Translator

  public static boolean isRegExValid(String emailAdress) {
    return GenericValidator.isEmail(emailAdress);
  }

  /**
   * verify if there is a mail server registered to the domain name. and return the email servers
   * count
   */
  public static int mailServersCount(String hostName) throws NamingException {
    Hashtable<String, String> env = new Hashtable<>();
    env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory");
    DirContext ictx = new InitialDirContext(env);
    Attributes attrs = ictx.getAttributes(hostName, new String[] {"MX"});
    Attribute attr = attrs.get("MX");
    if (attr == null) {
      return (0);
    }
    return (attr.size());
  }

  private static String className() {
    return BaseMessages.getString(PKG, "MailValidator.ClassName");
  }

  private static int hear(BufferedReader in) throws IOException {
    String line = null;
    int res = 0;

    while ((line = in.readLine()) != null) {
      String pfx = line.substring(0, 3);
      try {
        res = Integer.parseInt(pfx);
      } catch (Exception ex) {
        res = -1;
      }
      if (line.charAt(3) != '-') {
        break;
      }
    }

    return res;
  }

  private static void say(BufferedWriter wr, String text) throws IOException {
    wr.write(text + "\r\n");
    wr.flush();

    return;
  }

  private static ArrayList<String> getMX(String hostName) throws NamingException {
    // Perform a DNS lookup for MX records in the domain
    Hashtable<String, String> env = new Hashtable<>();
    env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory");
    DirContext ictx = new InitialDirContext(env);
    Attributes attrs = ictx.getAttributes(hostName, new String[] {"MX"});
    Attribute attr = attrs.get("MX");

    // if we don't have an MX record, try the machine itself
    if ((attr == null) || (attr.size() == 0)) {
      attrs = ictx.getAttributes(hostName, new String[] {"A"});
      attr = attrs.get("A");
      if (attr == null) {
        throw new NamingException(
            BaseMessages.getString(PKG, "MailValidator.NoMatchName", hostName));
      }
    }

    // Huzzah! we have machines to try. Return them as an array list
    // NOTE: We SHOULD take the preference into account to be absolutely
    // correct. This is left as an exercise for anyone who cares.
    ArrayList<String> res = new ArrayList<>();
    NamingEnumeration<?> en = attr.getAll();

    while (en.hasMore()) {
      String x = (String) en.next();
      String[] f = x.split(" ");
      if (f[1].endsWith(".")) {
        f[1] = f[1].substring(0, (f[1].length() - 1));
      }
      res.add(f[1]);
    }
    return res;
  }

  /**
   * Validate an email address This code is from : http://www.rgagnon.com/javadetails/java-0452.html
   */
  public static MailValidationResult isAddressValid(
      ILogChannel log,
      String address,
      String senderAddress,
      String defaultSMTPServer,
      int timeout,
      boolean deepCheck) {

    MailValidationResult result = new MailValidationResult();

    if (!isRegExValid(address)) {
      result.setErrorMessage(
          BaseMessages.getString(PKG, "MailValidator.MalformedAddress", address));
      return result;
    }

    // Find the separator for the domain name
    int pos = address.indexOf('@');

    // If the address does not contain an '@', it's not valid
    if (pos == -1) {
      return result;
    }

    if (!deepCheck) {
      result.setValide(true);
      return result;
    }

    // Isolate the domain/machine name and get a list of mail exchangers
    String domain = address.substring(++pos);

    // Maybe user want to switch to a default SMTP server?
    // In that case, we will ignore the domain
    // extracted from email address

    ArrayList<String> mxList = new ArrayList<>();
    if (Utils.isEmpty(defaultSMTPServer)) {
      try {
        mxList = getMX(domain);

        // Just because we can send mail to the domain, doesn't mean that the
        // address is valid, but if we can't, it's a sure sign that it isn't
        if (mxList == null || mxList.size() == 0) {
          result.setErrorMessage(
              BaseMessages.getString(PKG, "MailValidator.NoMachinesInDomain", domain));
          return result;
        }
      } catch (Exception ex) {
        result.setErrorMessage(
            BaseMessages.getString(
                PKG, "MailValidator.ErrorGettingMachinesInDomain", ex.getMessage()));
        return result;
      }
    } else {
      mxList.add(defaultSMTPServer);
    }

    if (log.isDebug()) {
      log.logDebug(
          BaseMessages.getString(PKG, "MailValidator.ExchangersFound", "" + mxList.size()));
    }

    // Now, do the SMTP validation, try each mail exchanger until we get
    // a positive acceptance. It *MAY* be possible for one MX to allow
    // a message [store and forwarder for example] and another [like
    // the actual mail server] to reject it. This is why we REALLY ought
    // to take the preference into account.
    for (int mx = 0; mx < mxList.size(); mx++) {
      boolean valid = false;
      BufferedReader rdr = null;
      BufferedWriter wtr = null;
      Socket skt = null;
      try {
        String exhanger = mxList.get(mx);
        if (log.isDebug()) {
          log.logDebug(
              className(), BaseMessages.getString(PKG, "MailValidator.TryingExchanger", exhanger));
        }

        int res;

        skt = new Socket(exhanger, 25);
        // set timeout (milliseconds)
        if (timeout > 0) {
          skt.setSoTimeout(timeout);
        }

        if (log.isDebug()) {
          log.logDebug(
              className(),
              BaseMessages.getString(
                  PKG, "MailValidator.ConnectingTo", exhanger, "25", skt.isConnected() + ""));
        }

        rdr = new BufferedReader(new InputStreamReader(skt.getInputStream()));
        wtr = new BufferedWriter(new OutputStreamWriter(skt.getOutputStream()));

        res = hear(rdr);
        if (res != 220) {
          throw new Exception(BaseMessages.getString(PKG, "MailValidator.InvalidHeader"));
        }

        // say HELLO it's me
        if (log.isDebug()) {
          log.logDebug(className(), BaseMessages.getString(PKG, "MailValidator.SayHello", domain));
        }
        say(wtr, "EHLO " + domain);
        res = hear(rdr);
        if (res != 250) {
          throw new Exception("Not ESMTP");
        }
        if (log.isDebug()) {
          log.logDebug(
              className(), BaseMessages.getString(PKG, "MailValidator.ServerReplied", "" + res));
        }

        // validate the sender address
        if (log.isDebug()) {
          log.logDebug(
              className(), BaseMessages.getString(PKG, "MailValidator.CheckSender", senderAddress));
        }
        say(wtr, "MAIL FROM: <" + senderAddress + ">");
        res = hear(rdr);
        if (res != 250) {
          throw new Exception(BaseMessages.getString(PKG, "MailValidator.SenderRejected"));
        }
        if (log.isDebug()) {
          log.logDebug(
              className(), BaseMessages.getString(PKG, "MailValidator.SenderAccepted", "" + res));
        }

        // Validate receiver
        if (log.isDebug()) {
          log.logDebug(
              className(), BaseMessages.getString(PKG, "MailValidator.CheckReceiver", address));
        }
        say(wtr, "RCPT TO: <" + address + ">");
        res = hear(rdr);

        // be polite
        say(wtr, "RSET");
        hear(rdr);
        say(wtr, "QUIT");
        hear(rdr);
        if (res != 250) {
          throw new Exception(
              BaseMessages.getString(PKG, "MailValidator.AddressNotValid", address));
        }

        if (log.isDebug()) {
          log.logDebug(
              className(),
              BaseMessages.getString(PKG, "MailValidator.ReceiverAccepted", address, "" + res));
        }
        valid = true;

      } catch (Exception ex) {
        // Do nothing but try next host
        result.setValide(false);
        result.setErrorMessage(ex.getMessage());
      } finally {
        if (rdr != null) {
          try {
            rdr.close();
          } catch (Exception e) {
            // ignore this
          }
        }
        if (wtr != null) {
          try {
            wtr.close();
          } catch (Exception e) {
            // ignore this
          }
        }
        if (skt != null) {
          try {
            skt.close();
          } catch (Exception e) {
            // ignore this
          }
        }

        if (valid) {
          result.setValide(true);
          result.setErrorMessage(null);
          if (log.isDebug()) {
            log.logDebug(className(), "=============================================");
          }
          return result;
        }
      }
    }
    if (log.isDebug()) {
      log.logDebug(className(), "=============================================");
    }

    return result;
  }
}
